package me.cabbit.digitone.gui;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import me.cabbit.digitone.tones.Screentone;
import me.cabbit.digitone.tones.ScreentoneListener;
import me.cabbit.digitone.tones.dots.DotsTonePanel;
import me.cabbit.digitone.tones.gradients.LinearGradientPanel;
import me.cabbit.digitone.tones.halftone.HalftonePanel;
import me.cabbit.digitone.tones.speedlines.SpeedlinesPanel;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * This class is the bridge between the interface ( {@link MainWindow} ) and
 * the screentones ({@link Screentone}).
 * 
 * @author Arkaid
 *
 */
public class ScreentoneMainWindow extends MainWindow implements ScreentoneListener, ChangeListener {
	private static final long serialVersionUID = 6216519552340790064L;

	//------------------------------------------------------------------//
	//--------------------------- properties ---------------------------//
	//------------------------------------------------------------------//

	/** Array of loaded screentones */
	private Screentone [] screentones;

	/** Currently selected (displaying) screentone */
	private Screentone selected;
	
	/** XML Document that contains all loaded presets */
	private Document xmlDoc;
	
	/** Node list that only contains the XML Elements that begin with
	 * <code><preset></code> */
	private NodeList presets;
	
	//------------------------------------------------------------------//
	//----------------------------- methods ----------------------------//
	//------------------------------------------------------------------//

	/**
	 * Constructor. Loads the screentones and presets
	 */
	public ScreentoneMainWindow() 
	{
		loadScreentones();
		loadPresets();
		
		//set default screentone
		this.selected = screentones[0];
		this.display.setScreentone(this.selected);
		
		//init default values
		this.toolbox.spnWidth.setValue(this.selected.getWidth());
		this.toolbox.spnHeight.setValue(this.selected.getHeight());
	
		//add listeners
		this.toolbox.spnWidth.addChangeListener(this);
		this.toolbox.spnHeight.addChangeListener(this);
	}
	
	/**
	 * Loads the presets and fills the presets combo box
	 */
	private void loadPresets() 
	{
		try 
		{
			//Java has a twisted way to open XMLs....
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			
			//if presets.xml does not exist, create an empty one
			//with one preset
			try 
			{
				this.xmlDoc = db.parse(new File("presets.xml"));
			} 
			catch (FileNotFoundException e) 
			{
				//make a new one
				FileWriter fw = new FileWriter("presets.xml");
				fw.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n");
				fw.write("<presetList>\n");
				fw.write("\t<preset minSize=\"5\" maxSize=\"5\" hSpacing=\"25\" invert=\"true\" angle=\"20\" name=\"Default Dots\" rowGap=\"50\" uid=\"D707DEA0FF5C4E6AAE7AC5E730C043D2\" vSpacing=\"25\"/>\n");
				fw.write("</presetList>\n");
				fw.close();
				this.xmlDoc = db.parse(new File("presets.xml"));
			}
			
			//get all the "preset" elements
			this.presets = this.xmlDoc.getElementsByTagName("preset");
		
			//add to combo
			for (int i = 0; i < this.presets.getLength(); i++) {
				Element e = (Element)this.presets.item(i);
				this.toolbox.cbxPresets.addItem(new Preset(e));
			}
			
			
		} catch (Exception e) {
			//TODO: handle this exception
			e.printStackTrace();
		}
		
	}

	/**
	 * Loads the screentones (panels). Ideallly the screentone and
	 * its interface (options panel) are separated into two classes
	 */
	private void loadScreentones() 
	{
		try 
		{
			//create the array and load screentones
			this.screentones = new Screentone []
			{
				new DotsTonePanel(),
				new LinearGradientPanel(),
				new HalftonePanel(),
				new SpeedlinesPanel()
			};
		} 
		//this might happen if a string file fails to load
		catch (FileNotFoundException exception)
		{
			//TODO: Display load string error message
			exception.printStackTrace();
		}
		
		//add screentones to combobox and options panel
		int len = this.screentones.length;
		for (int i = 0; i < len; i++) 
		{
			Screentone st = this.screentones[i];
			st.addScreentoneListener(this);
			this.toolbox.addScreentone(st);
		}
	}
	
	/**
	 * Shows a "Save File" dialog
	 * @param filterName name of the filter (ex: "PNG file")
	 * @param filterExtension extension for the filter (ex: ".png")
	 * @return if a file was selected, a File object representing that file
	 *         if the user cancelled, returns null
	 */
	protected File showSaveDialog(final String filterName, final String filterExtension)
	{
		//prepare the file chooser
		JFileChooser saveDialog = new JFileChooser();
		saveDialog.setDialogType(JFileChooser.SAVE_DIALOG);
		
		//set the filter
		saveDialog.setFileFilter(new FileFilter() {
			
			@Override
			public String getDescription() {
				return filterName;
			}
			
			@Override
			public boolean accept(File f) {
				return f.getName().endsWith(filterExtension);
			}
		});		
		
		
		//if the user cancelled, return null
		if (saveDialog.showDialog(this, "Save") != JFileChooser.APPROVE_OPTION)
			return null;

		//add extension if it doesn't have it
		File f = saveDialog.getSelectedFile();
		if (!f.getName().endsWith(filterExtension))
			f = new File(f.getAbsolutePath() + filterExtension);
		
		//return the file
		return f;
	}
	
	/**
	 * Helper method: Search all the presets for the one with the matching
	 * name and return the XML elemen that represents it
	 *
	 * @param name name of the preset (as shown in the combo box)
	 * @return the XML element with the preset information, when found, null
	 *		   otherwise
	 */	
	protected Element getPresetByName(String name) {
		
		boolean found = false;
		Element node = null;
		
		//loop through all the presets until a match is found
		for (int i = 0; i < this.presets.getLength() && !found; i++) 
		{
			node = (Element)this.presets.item(i);
			found = node.getAttribute("name").compareTo(name) == 0;
		}
		
		return found ? node : null;
	}
	
	/**
	 * Saves the xml document to a file
	 */
	protected void saveXMLPresetsFile() 
	{
		//save the xml document
		try {
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(this.xmlDoc);
			
			StreamResult result =  new StreamResult(new File("presets.xml"));
			transformer.transform(source, result);
		} catch (Exception ex)
		{
			//TODO: handle this exception
			ex.printStackTrace();
		}		
	}

	//------------------------------------------------------------------//
	//------------------- implemented abstract methods -----------------//
	//------------------------------------------------------------------//


	@Override
	protected void onMenuSavePNG() 
	{
		//Show save dialog
		File f = showSaveDialog("PNG file", ".png");
		
		//Do nothing on cancel
		if (f == null)
			return;
		
		//save
        try {
        	//ask the display to produce a buffered image and save it
			ImageIO.write(this.display.createBufferedImage(), "png", f);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	@Override
	protected void onPresetSave() 
	{
		//Pop up a dialog to write the preset name
		String ret = JOptionPane.showInputDialog(this,
				this.strings.get("DIALOG_PRESET_SAVE_1") +
				"\n" + 
				this.strings.get("DIALOG_PRESET_SAVE_2"),
				this.toolbox.cbxPresets.getSelectedItem());
		
		//cancelled
		if (ret == null)
			return;

		//determine if it's a node to be overwritten or a new one
		Element node = getPresetByName(ret);			
		
		
		//make a new node
		boolean isNewNode = node == null;
		if (isNewNode) {
			node = this.xmlDoc.createElement("preset");
			node.setAttribute("name", ret);
			node.setAttribute("uid", this.selected.getUID());
			this.xmlDoc.getElementsByTagName("presetList").item(0).appendChild(node);
		}
		
		//let the current screentone know it has to save
		this.selected.savePreset(node);
		
		//update the combo
		if (isNewNode) 
		{
			Preset p = new Preset(node);
			this.toolbox.cbxPresets.addItem(p);
			this.toolbox.cbxPresets.setSelectedItem(p);
		}
		
		//save xml
		saveXMLPresetsFile();
				
	}
	
	@Override
	protected void onPresetDelete() {
		//preset to delete
		Preset delPreset = (Preset) this.toolbox.cbxPresets.getSelectedItem();
		
		//Pop up a dialog to confirm deletion
		String msg = String.format(
						this.strings.get("DIALOG_PRESET_DELETE"), 
						delPreset);
		
		int result = JOptionPane.showConfirmDialog(this, msg, "", JOptionPane.YES_NO_OPTION);
		
		//nothing gets done
		if (result == JOptionPane.NO_OPTION)
			return;
		
		//delete from xml
		Element node = getPresetByName(delPreset.toString());
		node.getParentNode().removeChild(node);
		
		//save xml
		saveXMLPresetsFile();

		//update combo
		this.toolbox.cbxPresets.removeItem(delPreset);
		
	}

	@Override
	protected void onPresetSelected(Preset preset) 
	{
		//switch to preset
		String uid = preset.getUID();
		int i = 0;
		while (uid.compareTo(this.screentones[i].getUID()) != 0)
			i++;
		
		//select it
		this.selected = this.screentones[i];
		this.display.setScreentone(this.selected);
		
		//switch it in the combo box
		this.toolbox.cbxScreentones.setSelectedItem(this.selected);
		
		//load preset and repaint
		this.selected.loadPreset(preset.getXMLElement());
		this.propertyChanged();
	}
	
	@Override
	protected void onScreentoneSelected(Screentone st) 
	{
		//select it
		this.selected = st;
		this.display.setScreentone(this.selected);
		
		//repaint
		this.propertyChanged();		
	}
	
	//------------------------------------------------------------------//
	//------------------------ ScreentoneListener ----------------------//
	//------------------------------------------------------------------//

	@Override
	public void propertyChanged() {
		//Some screentone option changed, so force a refresh
		this.display.forceRender();
		this.display.repaint();
	}

	@Override
	public void buildProgressChanged(int progress) {}

	//------------------------------------------------------------------//
	//-------------------------- ChangeListener ------------------------//
	//------------------------------------------------------------------//

	@Override
	public void stateChanged(ChangeEvent event) {
		//change width and height
		if (event.getSource() == this.toolbox.spnWidth ||
			event.getSource() == this.toolbox.spnHeight)
		{
			this.selected.setSize(
					(int)this.toolbox.spnWidth.getValue(), 
					(int)this.toolbox.spnHeight.getValue());
		}
	}

}
